package csc411;

import java.io.*;
import java.net.*;
import java.util.Timer;
import java.util.TimerTask;

public class Client {

	private String[] args;
	private InetAddress serverAddress;
	private int serverPort;
	private DatagramSocket clientSocket;
	private int requestId;	// keeps track of no of request messages made by client
	private double packetLossRate = 0.5; // 50% chance of packet loss
	private int timeout = 5000; // timeout for server response in millisecond
	private Timer timer;
	private MonitorTimerTask timerTask;
	private boolean monitoring = false;
	private int clientPort = 9876; // default

	public Client(String[] args) {

		if (this.clientSocket != null) {
			this.clientSocket.close();
		}
		// get commandline args
		for (int i = 0; i < args.length; i = i + 2) {

			// get server address
			if (args[i].equals("-a")) {

				try {
					this.serverAddress = InetAddress.getByName(args[i + 1]);
				} catch (UnknownHostException e) {
					System.out.println("Unknown Host (" + e.toString() + ")");
				}
			}

			// get server port
			if (args[i].equals("-p")) {
				this.serverPort = Integer.parseInt(args[i + 1]);
			}

			// get packet loss rate
			if (args[i].equals("-r")) {
				this.packetLossRate = Double.parseDouble(args[i + 1]);
			}

			// get timeout
			if (args[i].equals("-t")) {
				this.timeout = Integer.parseInt(args[i + 1]);
			}

			// get client port
			if (args[i].equals("-c")) {
				this.clientPort = Integer.parseInt(args[i + 1]);
			}
		}

		this.requestId = 0;

		// print server settings
		System.out.println("[CLIENT SETTINGS]");
		System.out.println("-----------------");
		System.out.println("Client Port: " + this.clientPort);
		System.out.println("Server Address: " + this.serverAddress.getHostAddress());
		System.out.println("Server Port: " + this.serverPort);
		System.out.println("Packet Loss Rate: " + this.packetLossRate + "%  Timeout: " + this.timeout + "ms");
		System.out.println();

	}

	public static void main(String[] args) {

		Client client = new Client(args);

		BufferedReader br = new BufferedReader(
				new InputStreamReader(System.in));

		int output = -1;

		// get user input
		while (output != 0) {

			// print menu
			System.out.println();
			System.out.println("------------------------------------");
			System.out.println("    Welcome to XYZ Banking Menu    ");
			System.out.println("------------------------------------");
			System.out.println("   Available services:");
			System.out.println("1. Open Account   2. Close Account");
			System.out.println("3. Deposit        4. Withdraw");
			System.out.println("5. Check Balance  6. Transfer");
			System.out.println("7. Monitor Updates");
			System.out.println("------------------------------------");
			System.out.println("Enter your choice (1-7) or 0 to exit ");
			System.out.print("> ");
			try {

				// read input from keyboard
				int input = Integer.parseInt(br.readLine());

				// call appropriate client stub based on input
				output = client.selectMenu(input);

			} catch (IOException e) {
				System.out.println(e.toString());
				output = -1;
			} catch (Exception e) {
				System.out.println(e.toString());
				output = -1;
			}

		}
	}

	public int selectMenu(int input) {

		BufferedReader br = new BufferedReader(
				new InputStreamReader(System.in));

		// possible inputs from user
		String name;
		String password;
		int accountNum;
		Currency type;
		float amount;
		int accountNumToTransfer;

		// simple if/else to call client stub based on input
		if (input == 0) {

			// exit program
			System.out.println();
			System.out.println("Thank you for banking with us");
			System.out.println();
			return 0;

		} else if (input == 1) {

			/*Hariz 220210*/
			System.out.println();
			System.out.println("-----------------");
			System.out.println(" Open an account ");
			System.out.println("-----------------");
			// get necessary inputs from user
			try {
				// get name
				name = Client.getName(br);

				// get password
				password = Client.getPassword(br);

				// get currency type
				type = Client.getCurrencyType(br);

				// get initial acct balance
				amount = Client.getInitBalance(br);

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call open acct stub
			System.out.println("[CLIENT] Opening new account...");
			System.out.println();
			this.openAccount_stub(name, password, type, amount);

		} else if (input == 2) {

			// Nadiah (160310:1550)

			System.out.println();
			System.out.println("------------------");
			System.out.println(" Close an account ");
			System.out.println("------------------");
			// get necessary inputs from user
			try {
				// get name
				name = Client.getName(br);

				// get password
				password = Client.getPassword(br);

				// get account number
				accountNum = Client.getAccountNum(br);

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call close account stub
			System.out.println();
			System.out.println("[CLIENT] Closing account...");
			this.closeAccount_stub(name, password, accountNum);

		} else if (input == 3 || input == 4) {

			System.out.println();
			if (input == 3) {
				System.out.println("-----------------");
				System.out.println(" Account deposit ");
				System.out.println("-----------------");
			}

			if (input == 4) {
				System.out.println("------------------");
				System.out.println(" Account withdraw ");
				System.out.println("------------------");
			}


			// get necessary inputs from user
			try {
				// get name
				name = Client.getName(br);

				// get password
				password = Client.getPassword(br);

				// get account number
				accountNum = Client.getAccountNum(br);

				// get currency type
				type = Client.getCurrencyType(br);

				// get amount
				amount = Client.getAmount(br);
				if (input == 4) {
					amount = amount * (-1);
				}

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call withdraw/deposit stub
			System.out.println();
			if (input == 3) {
				System.out.println("[CLIENT] Processing deposit...");
			}

			if (input == 4) {
				System.out.println("[CLIENT] Processing withdrawal...");
			}
			this.depositWithdraw_stub(name, password, accountNum, type, amount);

		}
                else if (input == 5){
                    //Riz (230310)
                        System.out.println();
			System.out.println("-----------------------");
			System.out.println(" Check account balance ");
			System.out.println("-----------------------");
			// get necessary inputs from user
			try {
				// get name
				name = Client.getName(br);

				// get password
				password = Client.getPassword(br);

				// get account number
				accountNum = Client.getAccountNum(br);

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call close account stub
			System.out.println("[CLIENT] Retrieving balance...");
			System.out.println();
			this.checkBalance(name, password, accountNum);

                }
                else if (input == 6) {

			// Nadiah (220310:1930)

			System.out.println();
			System.out.println("-----------------------------");
			System.out.println(" Transfer to another account ");
			System.out.println("-----------------------------");
			// get necessary inputs from user
			try {
				// get name
				name = Client.getName(br);

				// get password
				password = Client.getPassword(br);

				// get account number
				accountNum = Client.getAccountNum(br);

				// get currency type
				type = Client.getCurrencyType(br);

				// get amount
				amount = Client.getAmount(br);

				// get account number to transfer
				accountNumToTransfer = Client.getAccountNumToTransfer(br);

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call transfer stub
			System.out.println();
			System.out.println("[CLIENT] Transfering to another account...");
			this.transfer_stub(name, password, accountNum, type, amount, accountNumToTransfer);

		} else if (input == 7) { // FEEZ 160310

			int interval = -1;

			System.out.println();
			System.out.println("------------------");
			System.out.println(" Monitor Updates ");
			System.out.println("------------------");
			// get necessary inputs from user
			try {

				// get desired monitor interval
				interval = Client.getInterval(br);

			} catch (Exception e) {
				System.out.println(e.getMessage());
				return -1;
			}

			// call stub
			this.monitorUpdates_stub(interval);

		} else {

			// invalid input
			System.out.println("[ERROR] Invalid input");
		}

		return -1;

	}

	// client stubs
	public int openAccount_stub(String name, String password,
			Currency type, float balance) {
		/*Hariz 220210*/
		int methodId = 1;

		Message sendMessage = new Message(this.requestId, methodId,
				name, password, type, balance);

		// increase request id
		this.requestId++;

		Message recvMessage;

		// create socket before sending/receiving
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// send message and receive response from server
		recvMessage = this.send(sendMessage);

		// display response from server
		System.out.println("[SERVER] " + new String(recvMessage.data));

		// close socket
		this.clientSocket.close();

		return 0;
	}

	// Nadiah (220210:1600)
	public int closeAccount_stub(String name, String password,
			int accountNum) {

		int methodId = 2;

		Message sendMessage = new Message(this.requestId, methodId,
				name, password, accountNum);

		// increase request id
		this.requestId++;

		Message recvMessage;

		// create socket before sending/receiving using a random port number
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// send message and receive response from server
		recvMessage = this.send(sendMessage);

		// display response from server
		System.out.println("[SERVER] " + new String(recvMessage.data));

		// close socket
		this.clientSocket.close();

		return 0;
	}

	public int depositWithdraw_stub(String name, String password,
			int accountNum, Currency type, float amount) {

		int methodId = 3;

		Message sendMessage = new Message(this.requestId, methodId,
				name, password, accountNum, type, amount);

		// increase request id
		this.requestId++;

		Message recvMessage;

		// create socket before sending/receiving using a random port number
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// send message and receive response from server
		recvMessage = this.send(sendMessage);

		// display response from server
		System.out.println("[SERVER] " + new String(recvMessage.data));

		// close socket
		this.clientSocket.close();

		return 0;
	}

	// Riz 250310
	public int checkBalance(String name, String password, int accountNum) {

            int methodId = 5;

		Message sendMessage = new Message(this.requestId, methodId,
				name, password, accountNum);

		// increase request id
		this.requestId++;

		Message recvMessage;

		// create socket before sending/receiving using a random port number
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// send message and receive response from server
		recvMessage = this.send(sendMessage);

		// display response from server
		System.out.println("[SERVER] " + new String(recvMessage.data));

		// close socket
		this.clientSocket.close();
            return 0;
	}

	// Nadiah (230310:1940)
	public int transfer_stub(String name, String password,
			int accountNum, Currency type, float amount, int accountNumToTransfer) {

		int methodId = 6;

		Message sendMessage = new Message(this.requestId, methodId,
				name, password, accountNum, type, amount, accountNumToTransfer);

		// increase request id
		this.requestId++;

		Message recvMessage;

		// create socket before sending/receiving using a random port number
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		// send message and receive response from server
		recvMessage = this.send(sendMessage);

		// display response from server
		System.out.println("[SERVER] " + new String(recvMessage.data));

		// close socket
		this.clientSocket.close();

		return 0;
	}

	// FEEZ 160310
	public int monitorUpdates_stub(int interval) {

		int methodId = 7;

		String response;

		Message sendMessage;
		Message recvMessage;

		byte[] receiveData = new byte[1024];

		/////////////////////////////////////////
		// send registration message to server //
		/////////////////////////////////////////
		sendMessage = new Message(this.requestId, methodId,
				interval);

		// increase request id
		this.requestId++;

		// create socket before sending/receiving using a random port number
		try {
			this.clientSocket = new DatagramSocket(this.clientPort);
		} catch (SocketException e) {
			System.out.println(e.toString());
		}

		System.out.println("[CLIENT] Registering client for update monitoring...");

		// send message and receive response from server //
		recvMessage = this.send(sendMessage);

		response = new String(recvMessage.data);

		// if unsuccessful, display response
		if (!response.equals("OK                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              ")) {
			System.out.println("[ERROR] Error in registration (" + response + ")");

			return -1;
		}


		// if successful, start monitoring
		System.out.println("[SERVER] Client successfully registered.");
		
		System.out.println("[CLIENT] Monitoring updates...");
		////////////////////////
		// monitoring updates //
		////////////////////////
		this.timer = new Timer();
		this.timerTask = new MonitorTimerTask();
		this.timer.schedule(timerTask, interval * 1000);
		this.monitoring = true;

		while (this.monitoring) {
			byte[] callbackData = new byte[1024];

			// start listening for incoming packets (blocking receive)
			DatagramPacket callbackPacket = new DatagramPacket(callbackData, callbackData.length);
			try {
				this.clientSocket.setSoTimeout(0);
				this.clientSocket.receive(callbackPacket);
			} catch (SocketException e) {
				if (e.getMessage().equals("Socket closed")) {
					System.out.println("[CLIENT] Connection successfully closed");
				}
				this.monitoring = false;
				this.timer.cancel();
			} catch (Exception e) {
				System.out.println(e.toString());
				this.monitoring = false;
				this.timer.cancel();
			}

			// display received data from server
			if (monitoring) {
				System.out.println("[SERVER] " + new String(callbackData));
			}

		}

		System.out.println("[CLIENT] Monitor period ended");

		// close socket
		this.clientSocket.close();

		return 0;
	}

	private class MonitorTimerTask extends TimerTask {

		public void run() {
			clientSocket.close(); // Note: this will throw a socketexception
			timer.cancel();		// ensure that it is once-only
		}
	}

	// the send() method sends Message to the server
	// simulates packet loss by performing a coin flip
	// and retransmits sent packet after not receiving any reply from server (timeout)
	public Message send(Message message) {

		boolean timeout = false;
		Message recvMessage = null;

		do {

			boolean isPacketLost = false;

			// create data packet for sending
			DatagramPacket sendPacket = new DatagramPacket(
					message.data, message.data.length,
					this.serverAddress, this.serverPort);

			// do a coin flip to simulate loss of request message
			isPacketLost = Utils.isDroppedPacket(packetLossRate);

			// send data packet if its not supposed to be lost
			if (!isPacketLost) {

				try {

					System.out.println("[CLIENT] Packet sent");
					this.clientSocket.send(sendPacket);

				} catch (IOException e) {
					System.out.println(e.toString());
				}

			} else {

				System.out.println("[CLIENT] Packet dropped");

			}

			// wait for response from server
			try {
				recvMessage = this.receive();

				timeout = false;

			} catch (SocketTimeoutException e) {

				System.out.println("[CLIENT] Timeout!");

				message.markRetransmit(); // tested and verified

				timeout = true;

			} catch (Exception e) {

				// unknown error

				System.out.println(e.toString());

				timeout = false;
			}

		} while (timeout);


		return recvMessage;
	}

	public Message receive() throws Exception {

		Message message = new Message();

		// create receiving buffer
		byte[] recvData = new byte[1024];

		// create receiving packet
		DatagramPacket recvPacket = new DatagramPacket(recvData, recvData.length);


		// set timeout for waiting for server response
		this.clientSocket.setSoTimeout(this.timeout);
		this.clientSocket.receive(recvPacket);


		message.data = recvData;

		return message;
	}

	// the below static methods are for getting and checking user inputs
	public static String getName(BufferedReader br) throws Exception {
		String name = "";

		try {
			System.out.print("Enter name > ");
			name = br.readLine();

			// do appropriate checks
			if (name.equals("")) {
				throw new Exception("[ERROR] Name cannot be blank.");
			}

		} catch (IOException e) {
			System.out.println("[ERROR] " + e.toString());
		}



		return name;
	}

	public static String getPassword(BufferedReader br) throws Exception {
		String password = "";

		try {
			System.out.print("Enter password (4 chars) > ");
			password = br.readLine();

			// do appropriate checks
			if (password.length() != 4) {
				throw new Exception("[ERROR] Password length must be 4 characters.");
			}

		} catch (IOException e) {
			System.out.println("[ERROR] " + e.toString());
		}

		return password;

	}

	public static int getAccountNum(BufferedReader br) throws Exception {
		int accountNum = 0;

		try {
			System.out.print("Enter account # > ");
			accountNum = Integer.parseInt(br.readLine());
			// do appropriate checks
		} catch (NumberFormatException e) {
			throw new Exception("[ERROR] Account # must be a number.");
		}

		return accountNum;
	}

	// Nadiah (220310:1920)
	public static int getAccountNumToTransfer(BufferedReader br) throws Exception {
		int accountNumToTransfer = 0;

		try {
			System.out.print("Enter account # to transfer > ");
			accountNumToTransfer = Integer.parseInt(br.readLine());
			// do appropriate checks
		} catch (NumberFormatException e) {
			throw new Exception("[ERROR] Account # must be a number.");
		}

		return accountNumToTransfer;
	}

	public static Currency getCurrencyType(BufferedReader br) throws Exception {
		Currency type = null;
		String input = "";
		try {
			System.out.print("Enter currency type (SGD or USD) > ");
			input = br.readLine();
			type = Currency.valueOf(input);
			// do appropriate checks
		} catch (IllegalArgumentException e) {
			throw new Exception("[ERROR] Please enter either SGD or USD.");
		}

		return type;
	}

	public static float getAmount(BufferedReader br) throws Exception {
		float amount = 0;
		try {
			System.out.print("Enter amount > ");
			amount = Float.parseFloat(br.readLine());
			// do appropriate checks
		} catch (IOException e) {
			System.out.println("[ERROR] " + e.getMessage());
		} catch (NumberFormatException e) {
			throw new Exception("[ERROR] Please enter a number.");
		}

		if (amount <= 0) {
			throw new Exception("[ERROR] Please enter a number larger than 0");
		}

		return amount;
	}

	/*Hariz 220210*/
	public static float getInitBalance(BufferedReader br) throws Exception {
		float amount = 0;
		try {
			System.out.print("Enter initial balance > ");
			amount = Float.parseFloat(br.readLine());
			// do appropriate checks
		} catch (IOException e) {
			System.out.println("[ERROR] " + e.toString());
		} catch (NumberFormatException e) {
			throw new Exception("[ERROR] Please enter a number.");
		}

		// FEEZ added 140310
		if (amount < 0) {
			throw new Exception("[ERROR] Please enter a number larger than or equal to 0");
		}

		return amount;
	}

	// FEEZ 160310
	public static int getInterval(BufferedReader br) throws Exception {
		int interval = 0;
		try {
			System.out.print("Enter desired monitor interval (in seconds) > ");
			interval = Integer.parseInt(br.readLine());
			// do appropriate checks
		} catch (IOException e) {
			System.out.println("[ERROR] " + e.toString());
		} catch (NumberFormatException e) {
			throw new Exception("[ERROR] Please enter a number.");
		}

		// FEEZ added 140310
		if (interval <= 0) {
			throw new Exception("[ERROR] Please enter a number larger than 0");
		}

		return interval;
	}
}
